﻿// (c) Norbert Huffschmid
// This source is subject to the Microsoft Public License (Ms-PL).
// Please see http://go.microsoft.com/fwlink/?LinkID=131993 for details.

using System;
using System.Collections;
using System.Collections.Generic;
using System.Threading;
using NProtocol;

namespace ShoutBox.Core
{
    /// <summary>
    ///  Mixer for 8kHz-16Bit-Mono encoded audio streams
    /// </summary>
    public class Mixer : IDisposable
    {
        private const int OUTGOING_SAMPLE_SIZE_MAX = 500;
        private const int IDLE_REMOVE_LIMIT = 10;


#if LOG4NET
        private static readonly log4net.ILog log = log4net.LogManager.GetLogger(System.Reflection.MethodBase.GetCurrentMethod().DeclaringType);
#endif

        private class InputChannel
        {
            public short[] Buffer;
            public int IdleCounter;

            public InputChannel(short[] samples)
            {
                this.Buffer = samples;
                this.IdleCounter = 0;
            }

            public void AddSamples(short[] samples)
            {
                short[] newBuffer = new short[this.Buffer.Length + samples.Length];
                this.Buffer.CopyTo(newBuffer, 0);
                samples.CopyTo(newBuffer, this.Buffer.Length);
                this.Buffer = newBuffer;

                this.IdleCounter = 0;
            }

            public void RemoveSamples(int count)
            {
                if (this.Buffer.Length > 0)
                {
                    if (count < this.Buffer.Length)
                    {
                        short[] newBuffer = new short[this.Buffer.Length - count];
                        Array.Copy(this.Buffer, count, newBuffer, 0, this.Buffer.Length - count);
                        this.Buffer = newBuffer;
                    }
                    else
                    {
                        Clear();
                    }

                    this.IdleCounter = 0;
                }
            }

            public void Clear()
            {
                this.Buffer = new short[0];

                this.IdleCounter = 0;
            }
        }


        private IPlatformAdapterAudioOut outputAdapter;
        private Dictionary<Guid, InputChannel> inputChannels;
        private Thread mixWorkerThread;
        private object syncRoot = new object();
        private bool disposed = false;


        /// <summary>
        /// Constructor
        /// </summary>
        /// <param name="outputAdapter">Platform adapter for audio output</param>
        public Mixer(IPlatformAdapterAudioOut outputAdapter)
        {
#if LOG4NET
            log.Debug("Constructor");
#endif
            this.outputAdapter = outputAdapter;
            this.inputChannels = new Dictionary<Guid, InputChannel>();

            this.mixWorkerThread = new Thread(new ThreadStart(MixWorker));
            this.mixWorkerThread.Start();
        }


        /// <summary>
        /// Mix given audio data with audio data from other session members
        /// </summary>
        /// <param name="sessionMember">Sender of this pcm data</param>
        /// <param name="pcmData">PCM data</param>
        public void PlayPcmData(ISessionMember sessionMember, byte[] pcmData)
        {
            // convert byte array to short array
            short[] shortData = new short[pcmData.Length / 2];
            for (int i = 0; i < pcmData.Length; i += 2)
            {
                shortData[i / 2] = (short) (pcmData[i] + (pcmData[i + 1] << 8));
            }

            lock (this.syncRoot)
            {
                if (this.inputChannels.ContainsKey(sessionMember.Id))
                {
                    // already known session member ==> append data to existing buffer
                    this.inputChannels[sessionMember.Id].AddSamples(shortData);
#if LOG4NET
                    log.Debug("Added " + pcmData.Length + " bytes to inputchannel of sessionMember " + sessionMember.Name);
#endif
                }
                else
                {
                    // new session member ==> create and fill buffer for it
#if LOG4NET
                    log.Debug("Create new inputchannel for sessionMember " + sessionMember.Name);
#endif
                    InputChannel inputChannel = new InputChannel(shortData);
                    this.inputChannels.Add(sessionMember.Id, inputChannel);
                }
            }
        }


        /// <summary>
        /// Release all resources
        /// </summary>
        public void Dispose()
        {
#if LOG4NET
            log.Debug("Dispose");
#endif
            Dispose(true);
            GC.SuppressFinalize(this);
        }


        protected virtual void Dispose(bool disposing)
        {
            if (!disposed)
            {
                if (disposing)
                {
                    // nothing to do here (thread terminates on set disposed flag)
                    // Windows Phone does not support Thread.Abort()!
                }
            }
            disposed = true;
        }


        private void MixWorker()
        {
            do
            {
                lock (this.syncRoot)
                {
                    // determine maximum buffer length of all available channels
                    int maxBufferLength = 0;
                    Guid[] channelIds = new Guid[this.inputChannels.Count];
                    this.inputChannels.Keys.CopyTo(channelIds, 0);
                    foreach (Guid id in channelIds)
                    {
                        InputChannel channel = this.inputChannels[id];
                        if (channel.Buffer.Length == 0)
                        {
                            channel.IdleCounter++;

                            if (channel.IdleCounter > IDLE_REMOVE_LIMIT)
                            {
                                // no action on this channel any more ==> remove it
#if LOG4NET
                                log.Debug("Remove channel for id " + id.ToString());
#endif
                                this.inputChannels.Remove(id);
                            }
                        }
                        else if (channel.Buffer.Length > maxBufferLength)
                        {
                            maxBufferLength = channel.Buffer.Length;
                        }
                    }

                    // determine number of outgoing samples
                    int sampleSize = (maxBufferLength > OUTGOING_SAMPLE_SIZE_MAX) ? OUTGOING_SAMPLE_SIZE_MAX : maxBufferLength;

                    if (sampleSize > 0)
                    {
                        // there are samples available to send them the platform adapter
                        byte[] pcmOut = new byte[sampleSize * 2];
                        for (int i = 0; i < sampleSize; i++)
                        {
                            int sumValue = 0;
                            int numberOfContributors = 0;

                            foreach (InputChannel channel in this.inputChannels.Values)
                            {
                                if (channel.Buffer.Length > i)
                                {
                                    // this buffer can contribute
                                    sumValue += channel.Buffer[i];
                                    numberOfContributors++;
                                }
                            }

                            short mixedValue = (short)(sumValue / numberOfContributors);

                            pcmOut[i * 2] = (byte)(mixedValue & 0x00ff); // LSB
                            pcmOut[i * 2 + 1] = (byte)(mixedValue >> 8); // MSB
                        }
#if LOG4NET
                        log.Debug("Sending " + pcmOut.Length + " bytes to platform adapter");
#endif
                        this.outputAdapter.PlayPcmData(pcmOut);

                        // remove sent samples from buffers
                        foreach (InputChannel channel in this.inputChannels.Values)
                        {
                            // remove sent buffer values
                            channel.RemoveSamples(sampleSize);
                        }
                    }
                }

                // divide by 8 would match exactly to 8kHz sampling rate ==> be faster
                Thread.Sleep(OUTGOING_SAMPLE_SIZE_MAX / 16);
            }
            while (!disposed);
        }
    }
}
